﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Diagnostics;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows;
using Xceed.Silverlight.Utils;

namespace Xceed.Silverlight.ListBox
{
  internal class ScrollManager : DependencyObject
  {
    private const int KillSwitchThreshold = 25;
    private const int RoundingThreshold = 6;

    #region Constructors

    internal ScrollManager( PanelBase parentPanel )
    {
      m_parentPanel = parentPanel;
    }

    #endregion

    #region IsScrollManagerActive Internal Property

    internal bool IsScrollManagerActive
    {
      get;
      private set;
    }

    #endregion

    #region TargetIndex Internal Property

    internal double TargetIndex
    {
      get;
      private set;
    }

    #endregion

    #region ActualScrollIndex Internal Property

    internal double ActualScrollIndex
    {
      get;
      set;
    }

    #endregion // ActualScrollIndex Property

    #region AnimatedActualScrollIndex Internal Property

    private static readonly DependencyProperty AnimatedActualScrollIndexProperty = DependencyProperty.Register(
        "AnimatedActualScrollIndex", 
        typeof( double ), 
        typeof( ScrollManager ),
        new PropertyMetadata( 0d, new PropertyChangedCallback( ScrollManager.OnAnimatedActualScrollIndexChanged ) ) );

    private double AnimatedActualScrollIndex
    {
      get
      {
        return ( double )this.GetValue( ScrollManager.AnimatedActualScrollIndexProperty );
      }
      set
      {
        this.SetValue( ScrollManager.AnimatedActualScrollIndexProperty, value );
      }
    }

    private static void OnAnimatedActualScrollIndexChanged( DependencyObject sender, DependencyPropertyChangedEventArgs e )
    {
      var scrollManager = sender as ScrollManager;

      scrollManager.ActualScrollIndex = ( double )e.NewValue;
      scrollManager.RaiseActualScrollIndexChanged();
    }

    #endregion // ActualScrollIndex Property

    #region AvertageItemSize Internal Property

    internal double AverageItemSize
    {
      get;
      set;
    }

    #endregion

    #region ActiveNavigationAction

    internal PanelNavigationActionBase ActiveNavigationAction
    {
      get;
      private set;
    }

    #endregion

    #region Internal Events

    internal event EventHandler ActualScrollIndexChanged;

    internal event EventHandler<ScrollCompletedEventArgs> ScrollCompleted;

    #endregion

    #region Internal Methods

    internal void ForceScrollIndex( double originalIndex, double forcedIndex )
    {
      double difference = forcedIndex - originalIndex;

      this.ActualScrollIndex = Math.Max( 0, this.ActualScrollIndex + difference );
      this.TargetIndex = Math.Max( 0, this.TargetIndex + difference );
      m_startIndex = Math.Max( 0, m_startIndex + difference );
    }

    internal double RoundIndexToClosestItem( double index )
    {
      //Rounding: If the position requested would display less than "x" Pixels of the item, round it down to next item.
      if( this.AverageItemSize > 0d )
      {
        double computedRoundingThreshold = ScrollManager.RoundingThreshold / this.AverageItemSize;
        double computedBottomRoundingThreshold = 1d - computedRoundingThreshold;

        double modulo = index % 1;

        if( modulo != 0d )
        {
          if( modulo > computedBottomRoundingThreshold )
          {
            index = Math.Ceiling( index );
          }
          else if( modulo < computedRoundingThreshold )
          {
            index = Math.Floor( index );
          }
        }
      }

      return index;
    }

    internal void ScrollToIndex( double index, int duration, IEasingFunction easingFunction, PanelNavigationActionBase navigationAction )
    {
      ListBox parentListBox = m_parentPanel.ParentListBox;

      // if ListBox is locked, do not scroll
      if( ( parentListBox != null ) && ( parentListBox.IsLocked ) )
        return;

      bool alreadyRunning = this.IsScrollManagerActive;

      if( alreadyRunning )
      {
        m_scrollStoryboard.Pause();
      }

      if( this.ActiveNavigationAction != navigationAction )
      {
        this.ActiveNavigationAction = navigationAction;
      }

      this.TargetIndex = index;
      m_startIndex = this.ActualScrollIndex;
      m_animationDuration = duration;
      m_animationEasingFunction = easingFunction;

      if( duration > 0 )
      {
        this.StartScrollStoryboard();

        this.IsScrollManagerActive = true;
      }
      else
      {
        this.StopScrollStoryboard();

        this.ActualScrollIndex = this.TargetIndex;

        this.RaiseActualScrollIndexChanged();
        this.RaiseScrollingCompleted();
      }
    }

    internal void UpdateAnimationSettings( double scrollableRange )
    {
      if( IsScrollManagerActive )
      {
        // Since scrollableRange may change in function of viewport size, check if target is still valid
        if( this.TargetIndex > scrollableRange )
        {
          this.UpdateScrollAnimationTarget( scrollableRange );
        }
        else if( this.ActiveNavigationAction != null )
        {
          double updatedScrollTarget = this.ActiveNavigationAction.CalculateTargetScrollIndex();

          if( !LayoutUtils.AreClose( updatedScrollTarget, this.TargetIndex ) )
          {
            this.UpdateScrollAnimationTarget( updatedScrollTarget );
          }
        }
      }
    }

    private void UpdateScrollAnimationTarget( double newTarget )
    {
#if SILVERLIGHT
      var animation = m_scrollStoryboard.Children[ 0 ] as DoubleAnimation;
#else
      var animation = m_scrollStoryboard.Children[ 0 ] as MutableDoubleAnimation; 
#endif

      if( ( animation != null ) && ( animation.From.HasValue ) && ( animation.To.HasValue ) )
      {
        double previousToValue = animation.To.Value;
        double previousFromValue = animation.From.Value;
        double normalizedElapsedTime = ( this.AnimatedActualScrollIndex - previousFromValue ) / ( previousToValue - previousFromValue );

        if( normalizedElapsedTime < 1d )
        {
          animation.To = newTarget;
          animation.From -= ( normalizedElapsedTime * ( newTarget - previousToValue ) ) / ( 1 - normalizedElapsedTime );
        }
      }

      this.TargetIndex = newTarget;
    }

    #endregion

    #region Private Methods

    private void Shutdown()
    {
      this.StopScrollStoryboard();

      if( !this.IsScrollManagerActive )
        return;

      //m_panelBase.ClearValue( TextOptions.TextHintingModeProperty );

      this.RaiseScrollingCompleted();
    }

    private void RaiseActualScrollIndexChanged()
    {
      if( this.ActualScrollIndexChanged != null )
      {
        this.ActualScrollIndexChanged( this, EventArgs.Empty );
      }
    }

    private void RaiseScrollingCompleted()
    {
      this.IsScrollManagerActive = false;

      double averageContainerSize = this.m_parentPanel.GetAverageContainerSize();

      // When animation is completed, the actual scroll index should be at the desired target.
      if( ( this.ActiveNavigationAction != null ) &&
        ( !LayoutUtils.AreClose( this.ActualScrollIndex * averageContainerSize, this.ActiveNavigationAction.CalculateTargetScrollIndex() * averageContainerSize ) ) )
      {
        this.AnimatedActualScrollIndex = this.ActiveNavigationAction.CalculateTargetScrollIndex();
      }

      if( this.ScrollCompleted != null )
      {
        this.ScrollCompleted( this, new ScrollCompletedEventArgs( this.ActiveNavigationAction ) );
      }

      this.ActiveNavigationAction = null;
    }

    private void StartScrollStoryboard()
    {
      this.StopScrollStoryboard();

#if SILVERLIGHT
      DoubleAnimation animation = new DoubleAnimation();
#else
      MutableDoubleAnimation animation = new MutableDoubleAnimation();
#endif
      animation.From = m_startIndex;
      animation.To = this.TargetIndex;
      animation.Duration = TimeSpan.FromMilliseconds( m_animationDuration );
      animation.EasingFunction = m_animationEasingFunction;

#if SILVERLIGHT
      Storyboard.SetTarget( animation, this );
      Storyboard.SetTargetProperty( animation, new PropertyPath( ScrollManager.AnimatedActualScrollIndexProperty ) );

      m_scrollStoryboard = new Storyboard();
#else
      animation.TargetObject = this;
      animation.TargetProperty = ScrollManager.AnimatedActualScrollIndexProperty;

      m_scrollStoryboard = new MutableStoryboard();
#endif
      m_scrollStoryboard.Children.Add( animation );
      m_scrollStoryboard.Completed += new EventHandler( this.OnScrollStoryboardCompleted );
      m_scrollStoryboard.Begin();
    }

    private void StopScrollStoryboard()
    {
      if( m_scrollStoryboard == null )
        return;

      m_scrollStoryboard.Completed -= new EventHandler( this.OnScrollStoryboardCompleted );
      m_scrollStoryboard.Pause();
      m_scrollStoryboard = null;
    }

    private void OnScrollStoryboardCompleted( object sender, EventArgs e )
    {
      this.Shutdown();
    }

    #endregion

    #region Private Fields

#if SILVERLIGHT
    private Storyboard m_scrollStoryboard;
#else
    private MutableStoryboard m_scrollStoryboard;
#endif

    private double m_startIndex;
    private int m_animationDuration;
    private IEasingFunction m_animationEasingFunction;

    private PanelBase m_parentPanel;

    #endregion
  }
}
